Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | /** * API route for serving original (uncropped) practice attachment files * * GET /api/curriculum/[playerId]/attachments/[attachmentId]/original * * Serves the original uncropped image file for a practice attachment. * If no original exists (legacy attachments or skipped crop), falls back * to the regular cropped file. * * Used when re-editing photos to start from the full original image * rather than cropping an already-cropped copy. */ import { readFile, stat } from 'fs/promises' import { NextResponse } from 'next/server' import { join } from 'path' import { eq } from 'drizzle-orm' import { db } from '@/db' import { practiceAttachments } from '@/db/schema' import { canPerformAction } from '@/lib/classroom' import { getUserId } from '@/lib/viewer' import { withAuth } from '@/lib/auth/withAuth' /** * GET - Serve original attachment file */ export const GET = withAuth(async (_request, { params }) => { try { const { playerId, attachmentId } = (await params) as { playerId: string; attachmentId: string } if (!playerId || !attachmentId) { return NextResponse.json({ error: 'Player ID and Attachment ID required' }, { status: 400 }) } // Authorization check const userId = await getUserId() const canView = await canPerformAction(userId, playerId, 'view') if (!canView) { return NextResponse.json({ error: 'Not authorized' }, { status: 403 }) } // Get attachment record const attachment = await db .select() .from(practiceAttachments) .where(eq(practiceAttachments.id, attachmentId)) .get() if (!attachment) { return NextResponse.json({ error: 'Attachment not found' }, { status: 404 }) } // Verify the attachment belongs to the specified player if (attachment.playerId !== playerId) { return NextResponse.json({ error: 'Attachment not found' }, { status: 404 }) } // Use original filename if available, otherwise fall back to cropped file const filename = attachment.originalFilename || attachment.filename // Build file path const filepath = join(process.cwd(), 'data', 'uploads', 'players', playerId, filename) // Check if file exists let fileStats try { fileStats = await stat(filepath) } catch { // If original file doesn't exist, fall back to cropped file if (attachment.originalFilename) { const fallbackPath = join( process.cwd(), 'data', 'uploads', 'players', playerId, attachment.filename ) try { fileStats = await stat(fallbackPath) // Use fallback path const fileBuffer = await readFile(fallbackPath) return new NextResponse(new Uint8Array(fileBuffer), { headers: { 'Content-Type': attachment.mimeType, 'Content-Length': fileStats.size.toString(), 'Cache-Control': 'private, max-age=31536000', }, }) } catch { console.error(`Attachment file not found: ${fallbackPath}`) return NextResponse.json({ error: 'File not found' }, { status: 404 }) } } console.error(`Attachment file not found: ${filepath}`) return NextResponse.json({ error: 'File not found' }, { status: 404 }) } // Read and serve file const fileBuffer = await readFile(filepath) return new NextResponse(new Uint8Array(fileBuffer), { headers: { 'Content-Type': attachment.mimeType, 'Content-Length': fileStats.size.toString(), 'Cache-Control': 'private, max-age=31536000', // Cache for 1 year (files are immutable) }, }) } catch (error) { console.error('Error serving original attachment:', error) return NextResponse.json({ error: 'Failed to serve attachment' }, { status: 500 }) } }) |